package com.example.gulimall.seckill.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.example.common.config.rabbit.order.OrderTransactionalConfig;
import com.example.common.constant.CouponConstant;
import com.example.common.entity.coupon.SeckillSessionEntity;
import com.example.common.entity.coupon.SeckillSkuRelationEntity;
import com.example.common.entity.member.MemberEntity;
import com.example.common.to.kill.SecKillOrderTo;
import com.example.common.to.kill.SecKillRedisTo;
import com.example.common.to.kill.SkuInfoTo;
import com.example.common.utils.GeneratorUtils;
import com.example.common.utils.GuLiMallThreadLocalPool;
import com.example.gulimall.seckill.feign.CouponFeignService;
import com.example.gulimall.seckill.feign.ProductFeignService;
import com.example.gulimall.seckill.service.SecKillService;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;


@Slf4j
@Service
public class SecKillServiceImpl implements SecKillService {

    @Value("${sec.kill.days}") private int days;
    @Autowired private RabbitTemplate rabbitTemplate;
    @Autowired private RedissonClient redissonClient;
    @Autowired private StringRedisTemplate redisTemplate;
    @Autowired private CouponFeignService couponFeignService;
    @Autowired private ProductFeignService productFeignService;

    @Override
    public void uploadSecKillSkuLatest() {
        List<SeckillSessionEntity> sessions =
                this.couponFeignService.uploadSecKillSkuLatest(this.days);

        if (CollectionUtil.isEmpty(sessions)) {
            log.info("{} 无上架的秒杀商品", LocalDateTime.now());
            return;
        }

        this.cacheSessions(sessions);
        this.cacheSku(sessions);
    }

    @Override
    public List<SecKillRedisTo> getCurrentSecKillSku() {
        // 确定当前时间所在的场次
        long current = new Date().getTime();
        Set<String> keys = this.redisTemplate.keys(CouponConstant
                .SESSION_CACHE_PREFIX.concat("*"));

        if (CollectionUtil.isEmpty(keys)) {
            return Collections.emptyList();
        }

        int length = CouponConstant.SESSION_CACHE_PREFIX.length();
        for (String key : keys) {
            String interval = key.substring(length);
            String[] intervalArr = interval.split(CouponConstant.REGEX);

            long start = Long.parseLong(intervalArr[0]);
            long end = Long.parseLong(intervalArr[1]);

            boolean condition = current >= start && current <= end;
            if (!condition) {
                continue;
            }

            List<String> skuSessionIds = this.redisTemplate.opsForList().range(key, -100, 100);

            if (CollectionUtil.isEmpty(skuSessionIds)) {
                return Collections.emptyList();
            }

            BoundHashOperations<String, String, Object> ops =
                    this.redisTemplate.boundHashOps(CouponConstant.SKU_CACHE_PREFIX);

            List<Object> skuObj = ops.multiGet(skuSessionIds);

            if (CollectionUtil.isEmpty(skuObj)) {
                return Collections.emptyList();
            }

            return skuObj.stream().map(
                    a -> JSONObject.parseObject(String.valueOf(a), SecKillRedisTo.class)
            ).collect(Collectors.toList());
        }
        return Collections.emptyList();
    }

    @Override
    public SecKillRedisTo getSkuInfo(Long skuId) {
        String suffix = String.valueOf(skuId);

        BoundHashOperations<String, String, String> ops =
                this.redisTemplate.boundHashOps(CouponConstant.SKU_CACHE_PREFIX);

        Set<String> keys = ops.keys();

        if (CollectionUtil.isEmpty(keys)) {
            return null;
        }

        for (String key : keys) {
            // 1_1 -> 获取到 skuId
            String suffixSkuId = getSuffixSkuId(key);

            if (!Objects.equals(suffixSkuId, suffix)) {
                continue;
            }

            String json = ops.get(key);
            SecKillRedisTo to = JSONObject.parseObject(json, SecKillRedisTo.class);

            return Objects.requireNonNull(to);
        }

        return null;
    }

    /**
     * 秒杀功能：流程
     *
     * 1. 判断登录
     * 2. 校验参数的合法性
     *  2.1 秒杀时间校验
     *  2.2 随机码校验
     *  2.3 对应关系校验
     *  2.4 幂等性校验
     * 3. 获取分布式信号量进行扣减
     * 4. 快速创建秒杀单，用户，订单号，商品  （发送MQ）
     * 5. 收货地址填写，
     * 6. 确认支付
     */
    @Override
    @SneakyThrows
    public String secKill(String killId, String key, Integer num) {
        BoundHashOperations<String, String, String> ops =
                this.redisTemplate.boundHashOps(CouponConstant.SKU_CACHE_PREFIX);

        String json = ops.get(killId);
        if (StrUtil.isBlank(json)) {
            return null;
        }

        // 校验参数的合法性
        SecKillRedisTo to = JSONObject.parseObject(json, SecKillRedisTo.class);
        long secStart = to.getStart(), secEnd = to.getEnd(), now = new Date().getTime();
        String randomCode = to.getRandomCode(),
                combination = to.getPromotionSessionId() + CouponConstant.REGEX + to.getSkuId();
        boolean condition =
                // 时间校验，当前时间一定是在开始时间和结束时间段内
                !(now >= secStart && secEnd <= now)
                // 如果传入的 key 与 redis 中存储随机码不一致，并且传入的场次 ID 与商品 ID 组合后于传入随机码不一致
                || (!Objects.equals(key, randomCode) && !Objects.equals(killId, combination))
                // 每人限购数量
                && (num > to.getSeckillLimit().intValue());
        if (condition) {
            return null;
        }

        // 当前用户是否购买过
        MemberEntity entity = GuLiMallThreadLocalPool.getMember();
        String boughtKey = entity.getId() + CouponConstant.REGEX + to.getSkuId();
        Boolean absent = this.redisTemplate.opsForValue().setIfAbsent(boughtKey,
                String.valueOf(num), secEnd - secStart, TimeUnit.MILLISECONDS);
        // 已经购买
        if (Objects.isNull(absent) || !absent) {
            return null;
        }

        // 分布式信号量判断
        RSemaphore semaphore = this.redissonClient
                .getSemaphore(CouponConstant.SKU_STOCK_SEMAPHORE.concat(key));
        boolean acquire = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
        if (!acquire) {
            return null;
        }
        // 发送消息到MQ中
        return createInstanceAndSend(to, entity.getId(), num);
    }

    private String getSuffixSkuId (String key) {
        int i = key.indexOf(CouponConstant.REGEX) + 1;
        int len = key.length() - i;
        char[] chars = key.toCharArray();
        return String.valueOf(chars, i, len);
    }

    /**
     * 保存所有时间段内的参与秒杀的活动
     *
     * @param sessions  秒杀活动信息
     */
    private void cacheSessions(List<SeckillSessionEntity> sessions) {

        sessions.forEach(
                a -> {
                    long start = a.getStartTime().getTime();
                    long end = a.getEndTime().getTime();
                    final String suffix = start + CouponConstant.REGEX + end;

                    final String key = CouponConstant.SESSION_CACHE_PREFIX.concat(suffix);

                    // 当前的活动已经放进去了，就不需要再放了
                    if (Objects.requireNonNull(this.redisTemplate.hasKey(key))) {
                        return ;
                    }

                    List<String> skuIdsValue = a.getRelationSkus().stream()
                            // sessionId_skuId
                            .map(b -> b.getPromotionSessionId() + CouponConstant.REGEX + b.getSkuId())
                            .collect(Collectors.toList());

                    this.redisTemplate.opsForList().leftPushAll(key, skuIdsValue);
                    this.redisTemplate.expire(key, (end - start), TimeUnit.MILLISECONDS);
                }
        );

    }

    /**
     *  将商品详细信息保存到 redis 中
     * @param sessions      查询的活动数据
     */
    private void cacheSku(List<SeckillSessionEntity> sessions) {
        sessions.forEach(
                a -> {
                    // 操作 redis hash
                    BoundHashOperations<String, Object, Object> ops =
                            this.redisTemplate.boundHashOps(CouponConstant.SKU_CACHE_PREFIX);
                    List<SeckillSkuRelationEntity> skuList = a.getRelationSkus();
                    // 远程查询sku信息
                    List<Long> skuIds = skuList.stream().map(SeckillSkuRelationEntity::getSkuId)
                            .collect(Collectors.toList());

                    Map<Long, SkuInfoTo> skuMap = this.productFeignService.getSkuInfo(skuIds)
                            .stream().collect(Collectors.toMap(SkuInfoTo::getSkuId, Function.identity()));

                    skuList.forEach(
                            b -> {
                                // 商品 sku 信息不能重复保存
                                String skuKey = b.getPromotionSessionId() + CouponConstant.REGEX + b.getSkuId();
                                if (Objects.requireNonNull(ops.hasKey(skuKey))){
                                    return ;
                                }

                                // 生成商品随机码
                                String code = GeneratorUtils.orderToken();

                                SecKillRedisTo redisTo = new SecKillRedisTo();
                                BeanUtils.copyProperties(b, redisTo);

                                redisTo.setSkuInfo(skuMap.get(b.getSkuId()))
                                        .setStart(a.getStartTime())
                                        .setEnd(a.getEndTime())
                                        .setRandomCode(code);

                                // 使用分布式信号量来减少并发
                                String semaphoreKey = CouponConstant.SKU_STOCK_SEMAPHORE.concat(code);
                                RSemaphore semaphore = this.redissonClient.getSemaphore(semaphoreKey);
                                semaphore.trySetPermits(b.getSeckillCount());

                                ops.put(skuKey, JSONObject.toJSONString(redisTo));
                            }
                    );

                }
        );
    }


    /**
     * 创建消息实例，并发送消息
     *
     * @param to        当前商品信息
     * @param memberId  当前用户ID
     * @param num       当前用户抢购数量
     * @return          订单号
     */
    private String createInstanceAndSend (SecKillRedisTo to, Long memberId, Integer num) {
        // 订单号 -> order.seckill.order -> (order-event-exchange)
        // -> order.seckill.order -> order.seckill.order.queue -> 消费者
        String orderSn = IdWorker.getTimeId();
        SecKillOrderTo order = new SecKillOrderTo()
                .setNum(num)
                .setOrderSn(orderSn)
                .setMemberId(memberId)
                .setSkuId(to.getSkuId())
                .setSeckillPrice(to.getSeckillPrice())
                .setPromotionSessionId(to.getPromotionSessionId());
        this.rabbitTemplate.convertAndSend(OrderTransactionalConfig.ORDER_EXCHANGE,
                OrderTransactionalConfig.SEC_KILL_KEY, order);
        return orderSn;
    }
}
